{ DONE : отключить редактирование выбора файла }
{ DONE : сделать проверку - файл с образом не находится на выбранном устройстве }
{ TODO : галочка "свободное место в выбранном каталоге меньше рекомендуемого" }
{ DONE : после выбора устройства/файла образа обнулять галочки }
{ TODO : после нажатия Старт дизаблить всё, кроме Стоп; после нажатия Стоп всё обратно енаблить и обнулять галочки }
{ TODO : Восстановление }
{ DONE : выдавать сообщения ошибок при проверке возможности старта }
{ DONE : дерево: логические разделы - подузлы extended }
{ TODO : свободное и рекомендуемое место с префиксами байт }
{ TODO -1 : два раза вызывается SetChecked (в toggleboxcontainer и в selectgrid) }

unit mainunut;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Process, FileUtil, LResources, Forms, Controls, Graphics,
  Dialogs, ComCtrls, StdCtrls, Buttons, EditBtn,
  TreeListView, ToggleBoxContainer, ExtCtrls, Spin, AsyncProcess;

const
  prBash = 'bash -c';
  prListDevs =   'parted -l -m';
  prGrep =  'grep --color=never';
  prGetLabels = 'blkid -o value -s LABEL';
  prGzip = 'gzip';
  prDD = 'dd';
  prDiskFree = 'df --block-size=1';
//  prGetStatus = 'kill -USR1';
  prWatch = 'pid=$(jobs -p) ; while ps -p $pid > /dev/null; do kill -USR1 $pid ; sleep 10; done';

  grepListDevs = ':';
  grepDiskFree = '/dev';

  gzipUnpack = '-cd';
  gzipPack = '-cn';

type

  { TMainForm }

  TMainForm = class(TForm)
    ALProcess: TAsyncProcess;
    BtnStop: TButton;
    CBRightParams: TCheckBox;
    cbIgnoreErrors: TCheckBox;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    lbErrors: TLabel;
    lbNeedSize: TLabel;
    lbFreeOnSelected: TLabel;
    lbSpeed: TLabel;
    lbProgress: TLabel;
    lbRemained: TLabel;
    PanelSources: TPanel;
    BtnBack: TBitBtn;
    BtnNext: TBitBtn;
    BTNExit: TBitBtn;
    BTNRescanDrives: TBitBtn;
    BTNStart: TButton;
    CBUnderstand: TCheckBox;
    DestFileName: TFileNameEdit;
    LProcess: TProcess;
    pbProgress: TProgressBar;
    RBBackup: TRadioButton;
    RBRestore: TRadioButton;
    PageControl1: TPageControl;
    GridSources: TTreeListView;
    EditBlockSize: TSpinEdit;
    CompLevel: TSpinEdit;
    TSStart: TTabSheet;
    TSDest: TTabSheet;
    TSSelectSource: TTabSheet;
    TSSelectAction: TTabSheet;
    procedure ALProcessReadData(Sender: TObject);
    procedure ALProcessTerminate(Sender: TObject);
    procedure BTNBackClick(Sender: TObject);
    procedure BTNNextClick(Sender: TObject);
    procedure BTNRescanDrivesClick(Sender: TObject);
    procedure BTNStartClick(Sender: TObject);
    procedure BtnStopClick(Sender: TObject);
    procedure CBUnderstandChange(Sender: TObject);
    procedure DestFileNameChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure GridSourcesSelect(sender: TObject; item: TTreeListItem);
    procedure PageControl1Change(Sender: TObject);
  private
    { private declarations }
    devs: TList;
    rout: TStringList;
    selSize: string;
    tmStart: TDateTime;
    selDev: string;
    procedure DoAfterTerminate;
    procedure DoBeforeStart;
    function GetSelectedDevice: string;
    procedure CleanDevs;
    procedure SelectPrt(Sender: TObject);
    procedure CanStart;
    procedure ResetCan;
  public
    { public declarations }
  end; 

var
  MainForm: TMainForm;

implementation

{ TMainForm }
const
  mulprefs = 'KMGTP';

function ConvertSize(sz: string): extended;
var
  i: integer;
begin
  i := 1;
  while (i <= Length(sz)) and (sz[i] in ['0'..'9', ',']) do
    inc(i);
  result := round(StrToFloat(Copy(sz, 1, i-1)) * (1 shl (Pos(sz[i], mulprefs) * 10)));
end;

function SizeInPercents(szp, szd: string): integer;
begin
  result := integer(round((100 * ConvertSize(szp)) / ConvertSize(szd)));
end;

function GetDev(dev: string): string;
var
  i: integer;
begin
  i := length(dev);
  while (i >= 1) and (dev[i] <> '/') do
    dec(i);
  result := copy(dev, i + 1, length(dev));
end;

function MakeBash(cmd: string): string;
begin
  result := prBash + ' ''' + cmd + '''';
  writeln(result);
end;

procedure TMainForm.BTNBackClick(Sender: TObject);
begin
  if PageControl1.ActivePageIndex > 0 then
  begin
//    PageControl1.ActivePage.TabVisible := false;
    PageControl1.SelectNextPage(false, true);
//    PageControl1.ActivePage.TabVisible := true;
  end;
end;

procedure TMainForm.ALProcessTerminate(Sender: TObject);
begin
  DoAfterTerminate;
end;

procedure TMainForm.ALProcessReadData(Sender: TObject);
var
  buf: string;
  nba: integer;
  prsz: integer;
begin
  buf := '';
  nba := ALProcess.Output.NumBytesAvailable;
  if nba > 0 then
  begin
    SetLength(buf, nba);
    ALProcess.Output.Read(buf[1], nba);
    rout.Text := buf;
    buf := rout.Strings[2];
    rout.DelimitedText := buf;
    prsz := SizeInPercents(rout.Strings[1], selSize);
    pbProgress.Position :=  prsz * 10; // maxvalue = 1000
    pbProgress.Caption := IntToStr(prsz);

    lbProgress.Caption := Format('Скопировано %s байт из %s', [rout.Strings[1], ConvertSize(selSize)]); //TODO нормальный размер
    lbSpeed.Caption := 'Скорость: ' + rout.Strings[7] + ' ' + rout.Strings[8];
    lbRemained.Caption := 'Осталось: ' + IntToStr(round(100 * (Now - tmStart) / prsz)) + 'секунд';

//    Memo1.Lines.Add(buf);

  end;
end;

procedure TMainForm.BTNNextClick(Sender: TObject);
begin
  if PageControl1.ActivePageIndex < PageControl1.PageCount -1 then
  begin
//    PageControl1.Pages[PageControl1.ActivePageIndex + 1].TabVisible := true;
//    PageControl1.ActivePage.TabVisible := false;
    PageControl1.SelectNextPage(true, true);
//    PageControl1.ActivePage.TabVisible := true;
  end;
end;

procedure TMainForm.BTNRescanDrivesClick(Sender: TObject);
var
  ou, lbl: TStringList;
  parms: TStringList;
  i, j, pst, psz: integer;
  dev, prt, sz, tp, lb, fl, szdev, st: string;
  prtex: boolean;
  item: TTreeListItem;
  tb: TToggleBoxContainer;
//  page: TTabSheet;
//  nc: TControl;
//  cb : TRadioButton;
begin
  ResetCan;
  if devs = nil then
    devs := TList.Create;


  LProcess.CommandLine := MakeBash(prListDevs + ' |' + prGrep + ' ' + grepListDevs);
  LProcess.Execute;
  ou := TStringList.Create;
  lbl := TStringList.Create;
  ou.LoadFromStream(LProcess.Output);

  parms := TStringList.Create;
  parms.Delimiter := ':';
  parms.StrictDelimiter := true;

  for i:= 0 to ou.Count - 1 do
  begin
    parms.DelimitedText := LeftStr(ou[i], length(ou[i]) - 1);
    if not (parms[0][1] in ['0'..'9']) then
    begin
      dev := parms[0];
      prt := dev;

      sz := parms[1];
      szdev := sz;
      lb := parms[6];
      tp := parms[5];
      fl := '';
      item := GridSources.Items.Add(dev);

      tb := TToggleBoxContainer.Create(PanelSources);
      tb.Parent := PanelSources;
      tb.Top := 0;
      tb.Left := 0;
      tb.Width := PanelSources.Width;
      tb.Height := 60;
      tb.Visible := false;
      tb.name := GetDev(dev);
      tb.Caption := dev;
      devs.Add(tb);
    end
    else
    begin
      prt := dev + parms[0];
      st := parms[1];
      sz := parms[3];
      tp := parms[4];
      LProcess.CommandLine := prGetLabels + ' ' + prt;
      LProcess.Execute;
      lbl.LoadFromStream(LProcess.Output);
      if lbl.Count >0 then
        lb := lbl[0]
      else
        lb := '';
      fl := parms[6];
//      item := GridSources.Items.Add(GridSources.Items.FindItemWithText(dev), prt);

      pst := SizeInPercents(st, szdev);
      psz := SizeInPercents(sz, szdev);
      if psz = 0 then psz := 1;
      tb := TToggleBoxContainer(PanelSources.FindChildControl(GetDev(dev)));
      if tp <> '' then // проверить, не логический ли диск
      begin
          prtex:=false;
          for j := 0 to tb.BoxCount - 1 do
              if (tb.Boxes[j].StartPercent <= pst) and
                 (tb.Boxes[j].StartPercent + tb.Boxes[j].Percentage >= psz) then
              begin
                tb := tb.Boxes[j];
                pst := trunc((pst - tb.StartPercent) * 100 / tb.Percentage);
                psz := trunc(psz * 100 / tb.Percentage);
                item := GridSources.Items.Add(GridSources.Items.FindItemWithText(tb.Caption), prt);
                prtex:=true;
                break;
              end;
      end;
      if not prtex then
        item := GridSources.Items.Add(GridSources.Items.FindItemWithText(dev), prt);
      tb := tb.AddBox(pst, psz);
      tb.Name := GetDev(prt);
      tb.Caption := prt;
    end;
    tb.OnClick := @SelectPrt;
    tp := UpperCase(tp);
    item.RecordItemsText[1] := lb;
    item.RecordItemsText[2] := sz;
    item.RecordItemsText[3] := tp;
    item.RecordItemsText[4] := fl;
  end;

  TToggleBoxContainer(devs[0]).Visible := true;

  lbl.Free;
  ou.Free;
end;

procedure TMainForm.BTNStartClick(Sender: TObject);
var
  ddopt: string;
  cmd: string;
begin
  DoBeforeStart;
  ddopt := ' bs=' + EditBlockSize.Text;
  if cbIgnoreErrors.Checked then
    ddopt := ddopt + ' conv=sync,noerror';

  cmd := prDD + ddopt + ' if=' + GetSelectedDevice;

  if CompLevel.Value > 0 then
    cmd := cmd + ' |' + prGzip + ' ' + gzipPack + ' -' + CompLevel.Text + ' > '
  else
    cmd := cmd + ' of=';
  cmd := cmd + DestFileName.Text + ' & ' + prWatch;
//  'ddrescue -d ' + GetSelectedDevice + ' ' + DestFileName.Text;

  ALProcess.CommandLine := MakeBash(cmd);

  ALProcess.Execute;

end;

procedure TMainForm.BtnStopClick(Sender: TObject);
begin
  ALProcess.Terminate(0);
//  DoAfterTerminate;
end;

procedure TMainForm.CBUnderstandChange(Sender: TObject);
begin
  CanStart;
end;

procedure TMainForm.DestFileNameChange(Sender: TObject);
var
  ou: TStringList;
  buf: string;
begin
  ResetCan;
  LProcess.CommandLine := MakeBash(prDiskFree + ' ' + DestFileName.Text + ' |' + prGrep + ' ' + grepDiskFree);
  ou := TStringList.Create;

  LProcess.Execute;
  ou.LoadFromStream(LProcess.Output);

  buf := ou.Strings[0];
  ou.Delimiter := ' ';
  ou.DelimitedText := buf;

  lbFreeOnSelected.Caption := 'Свободное место в выбранном каталоге: ' + ou.Strings[3] + ' байт'; //TODO: размер в к, М, Г

  selDev := ou.Strings[0];

  CanStart;

  ou.Free;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin

end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
//  CleanDevs;
end;

procedure TMainForm.GridSourcesSelect(sender: TObject; item: TTreeListItem);
var
  tb, tbb: TToggleBoxContainer;
  i: integer;
begin
  ResetCan;
  tb := TToggleBoxContainer(PanelSources.FindChildControl(GetDev(item.GetParentInList.Text)));
  if tb <> nil then
  begin
    for i := 0 to devs.Count - 1 do
      TToggleBoxContainer(devs[i]).Visible := false;
    tb.Visible := true;
    tbb := tb.FindBoxByCaption(item.Text);
    if tbb <> nil then
      tbb.Checked := true
    else
      tb.SetChecked(tb);
  end;
end;

procedure TMainForm.PageControl1Change(Sender: TObject);
var
  ou: TStringList;
  buf: string;
begin
  if PageControl1.ActivePage = TSStart then
  begin
//    ResetCan;
    CanStart;
    exit;
  end;

  ou := TStringList.Create;
  if PageControl1.ActivePage = TSDest then
    if GridSources.Selected <> nil then
    begin
      LProcess.CommandLine := MakeBash(prDiskFree + ' ' + GridSources.Selected.RecordItemsText[0] + ' |' + prGrep + ' ' + grepDiskFree);
      LProcess.Execute;
      ou.LoadFromStream(LProcess.Output);

      buf := ou.Strings[0];
      ou.Delimiter := ' ';
      ou.DelimitedText := buf;

      lbNeedSize.Caption := 'Рекомендуется: ' + ou.Strings[3] + ' байт'; //TODO флажок "пофиг на рекомендуемый размер"
    end;
  ou.Free;
end;

procedure TMainForm.DoAfterTerminate;
begin
  BTNStart.Enabled := true;
  BtnStop.Enabled := false;
  if rout <> nil then rout.Free;
end;

procedure TMainForm.doBeforeStart;
begin
  BTNStart.Enabled := false;
  BtnStop.Enabled := true;
  rout := TStringList.Create;
  rout.Delimiter := ' ';
  selSize := GridSources.Selected.RecordItemsText[2];
  tmStart := Now;
end;

function TMainForm.GetSelectedDevice: string;
begin
  if GridSources.Selected = nil then
    result := ''
  else
    result := GridSources.Selected.Text;
end;

procedure TMainForm.CleanDevs;
var
   i: integer;
begin
  GridSources.Items.Clear;
  for i := 0 to PanelSources.ControlCount - 1 do
    if PanelSources.Controls[i].ClassType = TToggleBoxContainer then
      TToggleBoxContainer(PanelSources.Controls[i]).Free;
  if devs <> nil then devs.Clear;
end;

procedure TMainForm.SelectPrt(Sender: TObject);
begin
//  if sender.ClassType = TShape then
//    sender := TShape(sender).Parent;
  GridSources.Selected := GridSources.Items.FindItemWithText(TToggleBoxContainer(Sender).Caption);
end;

procedure TMainForm.CanStart;
var
  r: string;
begin
  r := '';
  if ALProcess.Running then
    r := 'Процесс копирования запущен' + LineEnding;
  if DestFileName.FileName = '' then
    r := r + 'Имя файла образа не задано' + LineEnding;
  if GetSelectedDevice = '' then
    r := r + 'Не выбрано устройство' + LineEnding;
  if Pos(GetSelectedDevice, selDev) > 0 then
    r := r + 'Файл с образом не должен находиться на выбранном устройстве' + LineEnding;

  lbErrors.Caption := r;
  lbErrors.Visible := r <> '';
  CBUnderstand.Visible := r = '';
  CBRightParams.Visible := r = '';
  BTNStart.Enabled := (r = '') and CBRightParams.Checked and CBUnderstand.Checked;
end;

procedure TMainForm.ResetCan;
begin
  CBUnderstand.Checked := false;
  CBRightParams.Checked := false;
end;

initialization
  {$I mainunut.lrs}

  DecimalSeparator := ',';
end.

